<template>
  <div class="intro-wrap" @touchmove.prevent="() => { }" v-show="isShow">
    <div :class="['intro-block', options.blockCls, !canClickTargetArea && 'intro-event-enable']" :style="blockStyle">
    </div>
    <div ref="introTooltip" :class="['intro-tooltip', options.tipCls]" :style="tipStyle" v-show="isShowTip"
      @click.stop="() => { }">
      <div class="intro-tip" v-html="tip"></div>
      <div :class="['intro-arrow', mainPosition, subPosition]"></div>
      <div class="intro-btns" v-if="options.showBtn">
        <button class="intro-btn" @click="exit" v-if="options.isShowSkip" v-show="hasNext">
          {{ options.skipLable }}
        </button>
        <button class="intro-btn" @click="prev" v-show="hasPrev">
          {{ options.prevLable }}
        </button>
        <button class="intro-btn" @click="next" v-show="hasNext">
          {{ options.nextLabel }}<span v-if="isAutoNext">({{ autoNextTime }}秒)</span>
        </button>
        <button class="intro-btn" @click="exit" v-show="!hasNext">
          {{ options.exitLabel }}
        </button>
      </div>
    </div>
  </div>
</template>

<script>
import { ROOT, CLIENT_WIDTH, CLIENT_HEIGHT, reloadClient, isDef, isFn } from './utils/util';
//间距
const SPACE_LENGTH = 10;
const INTRO_ENABLE_CLS = 'intro-event-enable';
const INTRO_DISABLE_CLS = 'intro-event-disable';
export default {
  data() {
    return {
      isShow: false, //是否显示指引
      isShowTip: false, //是否显示提示
      steps: [], //指引数据
      stepIndex: 0, //当前index
      stepIndexList: [], //指引执行记录
      currentDom: null, //当前指引dom
      width: 0,
      height: 0,
      top: 0,
      left: 0,
      tip: '点击这里',
      mainPosition: 'right',
      subPosition: 'sub-top',
      options: {
        blockCls: '', //目标区域自定义样式
        tipCls: '', //提示框自定义样式
        targetClickEnable: false, //是否可以点击目标区域
        clickTargetNext: true, //是否点击目标区域进入下一步
        showBtn: true, //是否显示按钮
        isShowSkip: true, //是否显示跳过按钮
        scrollToTarget: true, //是否滚动到目标位置 如果目标在超出屏幕 需要开启
        nextLabel: '下一步',
        prevLable: '上一步',
        skipLable: '跳过',
        exitLabel: '完成',
        maskOpacity: 0.5,
        domAttrFlag: 'data-intro',
        autoNext: true, //是否自动下一步
        autoNextTime: 5, //自动下一步时间
      },
      autoNextTimer: null,
      autoNextTime: 0,
      isExecuting: false,
    };
  },
  computed: {
    //目标区域位置
    blockStyle() {
      return {
        width: this.width + 'px',
        height: this.height + 'px',
        top: this.top + 'px',
        left: this.left + 'px',
        boxShadow: `0 0 1px 2px transparent, 0 0 0 5000px rgb(0,0,0,${this.options.maskOpacity})`,
      };
    },
    //提示框位置
    tipStyle() {
      let style = {};
      if (this.mainPosition === 'bottom') {
        style.top = this.top + this.height + SPACE_LENGTH;
      } else if (this.mainPosition === 'top') {
        style.bottom = CLIENT_HEIGHT - this.top + SPACE_LENGTH;
      } else if (this.mainPosition === 'left') {
        style.right = CLIENT_WIDTH - this.left + SPACE_LENGTH;
      } else if (this.mainPosition === 'right') {
        style.left = this.left + this.width + SPACE_LENGTH;
      }

      if (this.subPosition === 'sub-h-center') {
        style.top = this.top + this.height / 2;
      } else if (this.subPosition === 'sub-v-center') {
        style.left = this.left + this.width / 2;
      } else if (this.subPosition === 'sub-bottom') {
        style.bottom = CLIENT_HEIGHT - (this.top + this.height);
      } else if (this.subPosition === 'sub-top') {
        style.top = this.top;
      } else if (this.subPosition === 'sub-left') {
        style.left = this.left;
      } else if (this.subPosition === 'sub-right') {
        style.right = CLIENT_WIDTH - (this.left + this.width);
      }
      for (const key in style) {
        style[key] = style[key] + 'px';
      }
      return style;
    },
    //有上一步
    hasPrev() {
      return this.stepIndexList.length > 0;
    },
    //有下一步
    hasNext() {
      return this.stepIndex < this.steps.length - 1;
    },
    //当前step数据
    stepData() {
      return this.steps[this.stepIndex] || {};
    },
    //是否可以点击目标区域
    canClickTargetArea() {
      return isDef(this.stepData.targetClickEnable) ? this.stepData.targetClickEnable : this.options.targetClickEnable;
    },
    canClickTargetNext() {
      return isDef(this.stepData.clickTargetNext) ? this.stepData.clickTargetNext : this.options.clickTargetNext;
    },
    isAutoNext() {
      return isDef(this.stepData.autoNext) ? this.stepData.autoNext : this.options.autoNext;
    },
    maxAutoNextTime() {
      return isDef(this.stepData.autoNextTime) ? this.stepData.autoNextTime : this.options.autoNextTime;
    },
  },
  mounted() {
    let self = this;
    function onResize() {
      reloadClient();
      if (self.isShow) {
        self.execute(self.stepIndex);
      }
    }
    window.addEventListener('resize', onResize);
    self.$once('hook:beforeDestory', function () {
      window.removeEventListener('resize', onResize);
    });
  },
  methods: {
    setPosition({ width, height, left, top }) {
      this.width = width;
      this.height = height;
      this.left = left;
      this.top = top;
      //通过对目标所在位置分析  计算出提示框需要展示的位置
      let l = left;
      let r = CLIENT_WIDTH - (left + width);
      let t = top;
      let b = CLIENT_HEIGHT - (top + height);
      let hMax = Math.max(l, r); //横向空间最大的方向
      let vMax = Math.max(t, b); //纵向空间最大的方向
      let max = Math.max(hMax, vMax); //所有空间最大的方向
      let mainPosition; //主方向 决定提示框所在的方向
      let subPosition; //次方向 决定提示框对齐的方向
      // horizontal / vertical
      if (vMax < CLIENT_HEIGHT / 2 && max === hMax) {
        mainPosition = hMax === l ? 'left' : 'right';
        subPosition = this.height > CLIENT_HEIGHT / 2 ? 'sub-h-center' : vMax === b ? 'sub-bottom' : 'sub-top';
      } else {
        mainPosition = vMax === t ? 'top' : 'bottom';
        subPosition = this.width > CLIENT_WIDTH / 2 ? 'sub-v-center' : hMax === r ? 'sub-left' : 'sub-right';
      }
      this.mainPosition = mainPosition;
      this.subPosition = subPosition;
    },
    start(steps, options = {}) {
      this.options = { ...this.options, ...options };
      this.steps = steps;
      this.stepIndex = -1;
      this.isShow = true;
      this.stepIndexList.splice(0);
      this.isExecuting = true;
      document.body.classList.add(INTRO_DISABLE_CLS);
      //添加监听
      this.addAreaListener();
      this.next();
    },
    //点击区域监听 通过window点击来确定是否点击当前区域
    addAreaListener() {
      window.addEventListener('click', this.clickTargetArea, true);
    },
    //清理事件
    removeAreaListener() {
      window.addEventListener('click', this.clickTargetArea, true);
    },
    clickTargetArea(e) {
      if (!this.isShow) {
        return;
      }
      const { clientX, clientY } = e;
      if (this.canClickTargetNext && this.inside(clientX, clientY)) {
        this.next();
      } else {
        const tooltip = this.$refs.introTooltip;
        const box = tooltip.getBoundingClientRect();
        if (!this.inside(clientX, clientY, box)) {
          e.stopPropagation();
        }
      }
    },
    //点是否在区域内
    inside(x, y, box) {
      box = box || this;
      return x >= box.left && x <= box.left + box.width && y >= box.top && y <= box.top + box.height;
    },
    //上一步
    async prev() {
      const { beforePrev } = this.stepData;
      if (isFn(beforePrev)) {
        beforePrev(() => {
          const index = this.stepIndexList.pop();
          this.execute(index);
        });
      } else {
        const index = this.stepIndexList.pop();
        this.execute(index);
      }
    },
    //下一步
    async next() {
      if (!this.hasNext) {
        this.exit();
        return;
      }
      let self = this;
      //如果不是第一次
      if (this.stepData.step) {
        const { beforeNext } = this.stepData;
        if (isFn(beforeNext)) {
          beforeNext(
            (stepCode) => {
              let index = this.steps.findIndex((v) => v.step === stepCode);
              if (index === -1) {
                index = self.stepIndex + 1;
              }
              this.stepIndexList.push(this.stepIndex);
              this.execute(index);
            },
            (step, stepInfo) => {
              this.updateStep(step, stepInfo);
            }
          );
          return;
        }
        this.stepIndexList.push(this.stepIndex);
      }
      this.execute(this.stepIndex + 1);
    },
    updateStep(step, stepInfo) {
      let stepObj = this.steps.find((v) => v.step === step);
      if (stepObj) {
        console.log(stepObj);
        stepObj.step = stepInfo.step || stepObj.step;
        stepObj.tip = stepInfo.tip || stepObj.tip;
      }
    },
    //执行
    async execute(stepIndex) {
      this.clearTimer();
      let { step, tip = this.tip } = this.steps[stepIndex];
      if (!step) {
        console.error('not find step');
        this.exit();
        return;
      }
      const currentDom = document.querySelector(`[${this.options.domAttrFlag}="${step}"]`);
      if (!currentDom) {
        console.error('not find dom');
        this.exit();
        return;
      }
      this.stepIndex = stepIndex;
      this.currentDom = currentDom;
      this.tip = tip;
      const { scrollToTarget } = this.options;
      let { width, height, top, left } = this.currentDom.getBoundingClientRect();
      //如果不在视窗范围内 纵向
      if (scrollToTarget && (top > CLIENT_HEIGHT || top < 0)) {
        let scrollTop = ROOT.scrollTop;
        //期望的位置 视窗底部bottom-200的位置
        let d = top - (CLIENT_HEIGHT - (height + 200));
        scrollTop += d;
        if (scrollTop > ROOT.scrollHeight + CLIENT_HEIGHT) {
          scrollTop = ROOT.scrollHeight - CLIENT_HEIGHT;
        } else if (scrollTop < 0) {
          scrollTop = 0;
        }
        ROOT.scrollTop = scrollTop;
      }
      //如果不在视窗范围内 横向
      if (scrollToTarget && (left > CLIENT_WIDTH || left < 0)) {
        let scrollLeft = ROOT.scrollLeft;
        //期望的位置 视窗底部bottom-200的位置
        let d = left - (CLIENT_WIDTH - (width + 50));
        scrollLeft += d;
        if (scrollLeft > ROOT.scrollWidth + CLIENT_WIDTH) {
          scrollLeft = ROOT.scrollWidth - CLIENT_WIDTH;
        } else if (scrollLeft < 0) {
          scrollLeft = 0;
        }
        ROOT.scrollLeft = scrollLeft;
      }
      const box = this.currentDom.getBoundingClientRect();
      this.setPosition(box);
      this.currentDom.classList.add(INTRO_ENABLE_CLS);
      this.isShowTip = false;
      const { afterEnter } = this.stepData;
      if (isFn(afterEnter)) {
        afterEnter((step, stepInfo) => {
          this.updateStep(step, stepInfo);
        });
      }
      setTimeout(() => {
        if (!this.isExecuting) {
          return;
        }
        this.isShowTip = true;
        this.executeAuto();
      }, 250);
    },
    executeAuto() {
      if (this.isAutoNext && this.hasNext) {
        this.autoNextTime = this.maxAutoNextTime;
        this.autoNextTimer = setInterval(() => {
          this.autoNextTime--;
          if (this.autoNextTime <= 0) {
            clearInterval(this.autoNextTimer);
            this.next();
          }
        }, 1000);
      }
    },
    //跳过/退出 清理数据
    exit() {
      this.clear();
      this.isShowTip = false;
      this.isShow = false;
      if (this.options.onComplete) {
        this.options.onComplete();
      }
    },
    //清除class 解除限制
    clear() {
      this.isExecuting = false;
      this.clearTimer();
      if (this.currentDom) {
        this.currentDom.classList.remove(INTRO_ENABLE_CLS);
        document.body.classList.remove(INTRO_DISABLE_CLS);
      }
      this.removeAreaListener();
    },
    clearTimer() {
      clearInterval(this.autoNextTimer);
    },
  },
};
</script>

<style scoped>
.intro-wrap {
  pointer-events: none;
  position: fixed;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  z-index: 9999;
}

.intro-block {
  position: absolute;
  z-index: 1;
  box-shadow: 0 0 1px 2px transparent, 0 0 0 5000px rgb(0 0 0 / 50%);
  transition: all 0.3s;
}

.intro-block::after {
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  bottom: 0;
  margin: auto;
  width: 100%;
  height: 100%;
  margin: -3px;
  padding: 2px;
  border: 1px dashed #fff;
  pointer-events: none;
}

.intro-tooltip {
  position: absolute;
  z-index: 2;
  width: 260px;
  padding: 10px 10px;
  border-radius: 4px;
  background-color: #fff;
  transition: all 0.3s;
  pointer-events: auto;
}

.intro-arrow {
  position: absolute;
  border: 6px solid white;
}

.intro-arrow.top {
  bottom: -10px;
  border-top-color: white;
  border-right-color: transparent;
  border-bottom-color: transparent;
  border-left-color: transparent;
}

.intro-arrow.bottom {
  top: -10px;
  border-top-color: transparent;
  border-right-color: transparent;
  border-bottom-color: white;
  border-left-color: transparent;
}

.intro-arrow.left {
  right: -10px;
  border-top-color: transparent;
  border-right-color: transparent;
  border-bottom-color: transparent;
  border-left-color: white;
}

.intro-arrow.right {
  left: -10px;
  border-top-color: transparent;
  border-right-color: white;
  border-bottom-color: transparent;
  border-left-color: transparent;
}

.intro-arrow.sub-top {
  top: 10px;
}

.intro-arrow.sub-bottom {
  bottom: 10px;
}

.intro-arrow.sub-left {
  left: 10px;
}

.intro-arrow.sub-right {
  right: 10px;
}

.intro-btns {
  margin-top: 10px;
  display: flex;
  align-items: center;
  justify-content: flex-end;
}

.intro-btns .intro-btn {
  position: relative;
  min-width: 40px;
  padding: 4px;
  background-color: #eee;
  border: 1px solid #ddd;
  box-shadow: 0 0 1px #ddd;
  outline: none;
  margin-left: 6px;
}

.intro-btns .intro-btn:active::after {
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.1);
}

.intro-auto-next {
  border-radius: 50%;
}
</style>
<style>
.intro-event-enable {
  pointer-events: auto;
}

.intro-event-disable {
  pointer-events: none;
  overflow: hidden;
}
</style>
